// * 原型继承：
// * 在 JS 中，对象有一个特殊的隐藏属性 [[prototype]] （规范中的命名），它要么为 null，要么就是对另一个对象的引用。被引用的对象称为“原型”。
// * 当我们中 object 中读取一个缺失的属性时，JS 会自动从原型中获取该属性。在编程中，这被称为“原型继承”。

// ********************************************************************************************************************

// * 属性 [[prototype]] 是内部的而且是隐藏的，但是有很多设置它的方式：

// * 1. 使用 __proto__
let animal = {
	eats: true,
	walk() {
		console.log('roaming the grass')
	},
}

let rabbit = {
	jumps: true,
}

rabbit.__proto__ = animal // 设置 [[prototype]]（或者说，原型）= animal

console.log(rabbit.jumps, rabbit.eats) // true true（读取 rabbit 没有的属性时，会自动从原型中获取）

rabbit.walk() // roaming the grass（从原型(animal)中获得的）

// ********************************************************************************************************************

// * 原型链可以很长：
let obj1 = {
	name: 'obj1',
	// __proto__: obj3, 形成闭环会报错
}

let obj2 = {
	age: 20,
	__proto__: obj1,
}

let obj3 = {
	skill: 'js',
	__proto__: obj2,
}

console.log(obj3.name) // obj1
console.log(obj3.age) // 20
console.log(obj3.skill) // js

// * 这里有两个限制：1.引用不能形成闭环，会报错误。2.__prop__的值可以是对象，也可以是null；而其他类型的值都会被忽略。
// * __proto__ 是 [[Prototype]] 因历史原因留下来的 getter/setter。
// * __proto__ 与内部的 [[Prototype]] 不一样。__proto__ 是 [[Prototype]] 的 getter（读取）/setter（设置） 的方式。
// * __proto__ 根据规范仅受浏览器环境的支持。但实际上，服务端在内的所有环境都支持它。
// * __proto__ 有些过时了，现代编程语言建议使用函数 Object.setPrototypeOf() / Object.getPrototypeOf() 来取代 __proto__ 去 set（设置）/get（读取） [[Prototype]]（原型）。

// ********************************************************************************************************************

// * 写入不使用原型：【原型仅用于读取属性。对于写入/删除操作会直接在对象上进行】

// * 例子1：
let objProto = {
	eats: true,
	walk() {
		console.log('我是objProto对象的方法')
	},
}

let objs = {
	__proto__: objProto,
}

// 这里的写入无效（原型中的 walk 不会改变），并没有修改到原型对象上的方法，写入/删除只会在自身生效。
// objs.walk = function () {
// 	console.log('我是objs对象的方法')
// }

// delete objs.walk 删除无效（原型中的 walk 不会改变）。

objs.walk() // 我是objs对象的方法

// * 例子2：访问器属性是个例外，因为赋值是由 setter 函数处理的。因此，写入此类属性实际上与调用函数相同。
let users = {
	name: 'John',
	surname: 'Smith',

	set fullName(value) {
		;[this.name, this.surname] = value.split(' ')
	},

	get fullName() {
		return `${this.name} ${this.surname}`
	},
}

let admins = {
	__proto__: users,
	isAdmin: true,
}

console.log(admins.fullName) // John Smith
admins.fullName = 'Pete Peterson'

console.log(admins.fullName) // Pete Peterson
console.log(users.fullName) // John Smith

// 上面没有修改到 users 对象中 name 和 surname 值的原因，是因为原型中的 访问器属性 与函数调用形式相同，
// 所以通过 admins 对象调用 users 原型对象中的 set 方法时，set 方法中的 this 实际上指向的 admins 对象。

console.log(admins) // { isAdmin: true, name: 'Pete', surname: 'Peterson' }

// ********************************************************************************************************************

// * this 的值

// * 上面例子2中，有个有趣的问题。set 中的 this 到底指向 users 还是 admins。
// * 答案很简单，this 根本不受原型影响。
// * 无论在哪里找到方法，一个对象还是原型中（无论原型层级多深）。在一个方法调用中 this 始终是点符号 . 前面的对象。
// * 因此，setter 调用 admins.fullName = 使用 admins 作为 this，而不是 users。

// 例子：
let o0 = {
	like() {
		this.eat = true
	},
}

let o1 = {
	walk() {
		if (!this.isSleeping) {
			console.log('I walk')
		}
	},
	sleep() {
		this.isSleeping = true
	},
	__proto__: o0,
}

let o2 = {
	name: 'O2',
	__proto__: o1,
}

o2.sleep() // 调用原型中的方法，但是 this 指向的是 o2。
o2.like()

console.log(o2.isSleeping) // true
console.log(o2.eat) // true

console.log(o1.isSleeping) // undefined
console.log(o0.eat) // undefined

// ********************************************************************************************************************

// * for...in 循环也会迭代（遍历）自身属性和继承的属性。
let oo = {
	a: 1,
	b: 2,
	// toString / hasOwnProperty 是不可枚举的，它们属性标志为 { enumerable: false }，所以不会遍历出来。
	// __proto__: Object.prototype = { toString: function, hasOwnProperty: function, ..., [[prototype]]: null }
}

let cc = {
	c: 3,
	__proto__: oo,
}

for (const key in cc) {
	console.log(key) // c a b
}

// * 使用 obj.hasOwnProperty(key) 内建方法，可以判断对象是否拥有某个属性（自会从自身查找，不管原型）。
for (const key in cc) {
	// 是否是对象自身的属性
	if (cc.hasOwnProperty(key)) {
		console.log('对象自身的属性：', key) // c
	} else {
		console.log('原型中的属性：', key) // a b
	}
}

// ********************************************************************************************************************

// * 除了 for...in 会迭代继承属性外，所有其他的键/值获取方法仅对对象自身起作用。
// * 例如：Object.keys() 和 Object.values() 都会忽略继承属性，只对对象自身进行操作。

// * 总结：
// * 1. 在 JS 中有一个隐藏属性 [[prototype]] 它是对象的原型，它的值要么是 null 要么是另一个对象。我们可以使用 obj.__proto__ 来设置/访问它。
// * 2. 我们是从对象还是从原型链获取属性都是没区别的，获取速度都是一样的快，因为引擎会记住在哪里找到该属性，并在下一次请求中重用它。
